Skip to content

feat(btcindexer): finalize redeems on Sui using BTC confirmations#333

Merged
sczembor merged 15 commits intomasterfrom
stan/confirmations
Feb 5, 2026
Merged

feat(btcindexer): finalize redeems on Sui using BTC confirmations#333
sczembor merged 15 commits intomasterfrom
stan/confirmations

Conversation

@sczembor
Copy link
Contributor

@sczembor sczembor commented Feb 3, 2026

Description

Closes: #253


Author Checklist

All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.

I have...

  • included the correct type prefix in the PR title
  • added ! to the type prefix if API or client breaking change
  • added appropriate labels to the PR
  • provided a link to the relevant issue or specification
  • added a changelog entry to CHANGELOG.md
  • included doc comments for public functions
  • updated the relevant documentation or specification
  • reviewed "Files changed" and left comments if necessary

Summary by Sourcery

Implement unified confirmation handling for BTC mint and redeem flows and wire Sui redeem finalization based on verified BTC confirmations and reorg detection.

New Features:

  • Add Sui RPC and client support to finalize redeem requests on-chain using BTC transaction Merkle proofs and update local redeem/UTXO state.
  • Introduce retrieval and processing of confirming redeem requests from Sui indexer storage, keyed by BTC network, to drive redeem finalization batches from the BTC indexer.

Bug Fixes:

  • Prevent mint and redeem finalization of BTC transactions that have been reorged by validating stored block hashes against current chain data before finalizing.

Enhancements:

  • Refactor BTC confirmation logic into reusable helpers that categorize confirming transactions per-network and split finalized mints into active vs inactive keys independently of confirmation depth.
  • Extend Sui indexer storage and models with light-client contract metadata and richer redeem status transitions, including confirming, reorg, and finalized states.
  • Adjust BTC indexer cron flow to derive chain tips per-network from storage instead of passing a global latest height parameter.
  • Update tests to cover active/inactive mint splitting and new redeem finalization wiring while removing obsolete reorg and confirmation selection tests.

Tests:

  • Add and update unit tests around transaction activity splitting and BTC indexer confirmation handling, and extend test helpers to exercise the new multi-item redeem finalization RPC.

Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
@sczembor sczembor requested a review from a team as a code owner February 3, 2026 16:57
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 3, 2026

Reviewer's Guide

Refactors BTC indexer confirmation/finalization logic to support both mint and redeem flows with shared reorg/confirmation handling, and wires Sui indexer RPC/storage to fetch confirming redeems, update their status, and finalize them on Sui using generated Merkle proofs.

Sequence diagram for redeem finalization with shared BTC confirmation logic

sequenceDiagram
  actor Cron
  participant BtcIndexer as Indexer
  participant BtcDB as CFStorage
  participant SuiRPC as SuiIndexerRpc
  participant SuiDB as D1Storage
  participant SuiClient as SuiClientImp
  participant SuiChain as Sui_network

  Cron->>BtcIndexer: updateConfirmationsAndFinalize()
  BtcIndexer->>BtcIndexer: verifyConfirmingBlocks()

  Note over BtcIndexer: processRedeemFinalization
  BtcIndexer->>BtcIndexer: collect btcNetworks from packageConfigs
  loop per btcNetwork
    BtcIndexer->>BtcDB: getChainTip(network)
    alt chainTip is null
      BtcIndexer-->>BtcIndexer: skip network
    else chainTip exists
      BtcIndexer->>SuiRPC: getConfirmingRedeems(network)
      SuiRPC->>SuiDB: getConfirmingRedeems(network)
      SuiDB-->>SuiRPC: ConfirmingRedeemReq[]
      SuiRPC-->>BtcIndexer: ConfirmingRedeemReq[]

      BtcIndexer->>BtcIndexer: build ConfirmingTxCandidate[]
      BtcIndexer->>BtcIndexer: categorizeConfirmingTxs(candidates, chainTips)
      BtcIndexer-->>BtcIndexer: reorged[], finalized[]

      loop each reorged
        BtcIndexer->>SuiRPC: updateRedeemStatus(redeemId, Reorg)
        SuiRPC->>SuiDB: updateRedeemStatus(redeemId, Reorg)
      end

      BtcIndexer->>BtcIndexer: groupTransactionsByBlock(finalized)
      BtcIndexer-->>BtcIndexer: Map blockHash -> redeems

      loop per blockHash
        BtcIndexer->>BtcDB: fetchAndVerifyBlock(blockHash)
        BtcDB-->>BtcIndexer: block, merkleTree

        loop per redeem in block
          BtcIndexer->>BtcIndexer: find txIndex in block
          BtcIndexer->>BtcIndexer: getTxProof(merkleTree, tx)
          BtcIndexer-->>BtcIndexer: proofHex[]
          BtcIndexer->>BtcIndexer: add FinalizeRedeemItem to batch
        end

        alt batch not empty
          BtcIndexer->>SuiRPC: finalizeRedeems(batch)
          SuiRPC->>SuiDB: getRedeemWithSetup(redeemId) for each
          SuiDB-->>SuiRPC: RedeemRequest
          SuiRPC->>SuiClient: finalizeRedeem(FinalizeRedeemCall)
          SuiClient->>SuiChain: signAndExecuteTransaction(finalizeRedeem)
          SuiChain-->>SuiClient: digest (success)
          SuiClient-->>SuiRPC: digest
          SuiRPC->>SuiDB: setRedeemFinalized(redeemId)
        end
      end
    end
  end
  BtcIndexer-->>Cron: done
Loading

Class diagram for updated BTC and Sui indexer confirmation/finalization flow

classDiagram

class Indexer {
  - CFStorage storage
  - SuiIndexerRpc suiIndexer
  - Map~string, DepositInfo~ nbtcDepositAddrMap
  - Map~number, PackageConfig~ #packageConfigs
  - number confirmationDepth
  + updateConfirmationsAndFinalize() Promise~void~
  + processMintingFinalization() Promise~void~
  + processRedeemFinalization() Promise~void~
  - isReorged(height number, oldHash string, network BtcNet) Promise~boolean~
  - categorizeConfirmingTxs(txs ConfirmingTxCandidate~T~[], chainTips Map~BtcNet, number~) Promise~ReorgFinalizeResult~
  + splitActiveInactiveTxs(pendingTxs PendingTx[]) ActiveInactiveResult
  - groupTransactionsByBlock(items FinalizedRedeemLike[]) Map~string, FinalizedRedeemLike[]~
  - fetchAndVerifyBlock(blockHash string) Promise~VerifiedBlockData or null~
  - getTxProof(merkleTree MerkleTree, tx Transaction) string[] or null
}

class ConfirmingTxCandidate~T~ {
  + string or number id
  + number blockHeight
  + string blockHash
  + BtcNet network
  + T original
}

class ActiveInactiveResult {
  + string[] activeTxIds
  + string[] inactiveTxIds
}

class ReorgFinalizeResult~T~ {
  + ConfirmingTxCandidate~T~[] reorged
  + ConfirmingTxCandidate~T~[] finalized
}

class PendingTx {
  + string tx_id
  + number block_height
  + string or null block_hash
  + BtcNet btc_network
  + string deposit_address
}

class DepositInfo {
  + number setup_id
  + boolean is_active
}

class PackageConfig {
  + boolean is_active
  + BtcNet btc_network
}

class SuiIndexerRpc {
  <<interface>>
  + finalizeRedeems(requests FinalizeRedeemItem[]) Promise~void~
  + putRedeemTx(setupId number, suiTxId string, e RedeemRequestEventRaw) Promise~void~
  + getBroadcastedRedeemTxIds(network string) Promise~string[]~
  + confirmRedeem(txIds string[], blockHeight number, blockHash string) Promise~void~
  + redeemsBySuiAddr(setupId number, suiAddr string) Promise~RedeemRequestResp[]~
  + getConfirmingRedeems(network string) Promise~ConfirmingRedeemReq[]~
  + updateRedeemStatus(redeemId number, status RedeemRequestStatus) Promise~void~
}

class RPC {
  <<implements SuiIndexerRpc>>
  - Env env
  + finalizeRedeems(requests FinalizeRedeemItem[]) Promise~void~
  + updateRedeemStatus(redeemId number, status RedeemRequestStatus) Promise~void~
  + getConfirmingRedeems(network string) Promise~ConfirmingRedeemReq[]~
  + putRedeemTx(setupId number, suiTxId string, e RedeemRequestEventRaw) Promise~void~
  + getBroadcastedRedeemTxIds(network string) Promise~string[]~
  + confirmRedeem(txIds string[], blockHeight number, blockHash string) Promise~void~
  + redeemsBySuiAddr(setupId number, suiAddr string) Promise~RedeemRequestResp[]~
}

class D1Storage {
  - D1Database db
  + getConfirmingRedeems(network string) Promise~ConfirmingRedeemReq[]~
  + updateRedeemStatus(redeemId number, status RedeemRequestStatus) Promise~void~
  + setRedeemFinalized(redeemId number) Promise~void~
  + getRedeemWithSetup(redeemId number) Promise~RedeemRequest or null~
  + getPendingRedeems() Promise~RedeemRequest[]~
  + getBroadcastedRedeems() Promise~RedeemRequest[]~
}

class RedeemRequest {
  + number redeem_id
  + number setup_id
  + string redeemer
  + Uint8Array recipient_script
  + string amount
  + RedeemRequestStatus status
  + number created_at
  + string nbtc_pkg
  + string nbtc_contract
  + string lc_pkg
  + string lc_contract
  + SuiNet sui_network
}

class RedeemRequestRow {
  + number redeem_id
  + number setup_id
  + string redeemer
  + ArrayBuffer or Uint8Array recipient_script
  + string amount
  + number created_at
  + string nbtc_pkg
  + string nbtc_contract
  + string lc_pkg
  + string lc_contract
  + string sui_network
}

class ConfirmingRedeemReq {
  + number redeem_id
  + string btc_tx
  + number btc_block_height
  + string btc_block_hash
  + string btc_network
}

class FinalizeRedeemItem {
  + number redeemId
  + string[] proof
  + number height
  + number txIndex
}

class FinalizeRedeemCall {
  + number redeemId
  + string[] proof
  + number height
  + number txIndex
  + string nbtcPkg
  + string nbtcContract
  + string lcContract
  + string lcPkg
}

class SuiClient {
  <<interface>>
  + proposeRedeemUtxos(args ProposeRedeemCall) Promise~string~
  + solveRedeemRequest(args SolveRedeemCall) Promise~string~
  + finalizeRedeem(args FinalizeRedeemCall) Promise~string~
  + requestIkaPresign() Promise~string~
  + requestInputSignature(redeemId number, inputIndex number) Promise~string~
}

class SuiClientImp {
  - SuiClientInner #sui
  - Keypair signer
  + finalizeRedeem(args FinalizeRedeemCall) Promise~string~
  + proposeRedeemUtxos(args ProposeRedeemCall) Promise~string~
  + solveRedeemRequest(args SolveRedeemCall) Promise~string~
  + requestIkaPresign() Promise~string~
  + requestInputSignature(redeemId number, inputIndex number) Promise~string~
}

Indexer --> SuiIndexerRpc : uses
Indexer --> ConfirmingTxCandidate : creates
Indexer --> PendingTx : finalizes
Indexer --> DepositInfo : checks activity
Indexer --> PackageConfig : reads
RPC --> D1Storage : uses
RPC --> SuiClientImp : uses via createSuiClients
RPC ..|> SuiIndexerRpc
D1Storage --> RedeemRequestRow : maps from
D1Storage --> RedeemRequest : returns
D1Storage --> ConfirmingRedeemReq : returns
SuiClientImp ..|> SuiClient
SuiClientImp --> FinalizeRedeemCall : uses
RPC --> FinalizeRedeemItem : consumes
RPC --> ConfirmingRedeemReq : consumes
Loading

File-Level Changes

Change Details Files
Refactor confirmation/finalization flow in BTC indexer to separate mint and redeem processing and centralize confirmation/reorg logic.
  • Introduce ConfirmingTxCandidate helper type and categorizeConfirmingTxs/isReorged helpers to detect reorged vs finalized transactions across networks based on chain tips and confirmation depth.
  • Split updateConfirmationsAndFinalize into processMintingFinalization and processRedeemFinalization, where minting uses shared confirmation categorization and existing D1 storage, and redeem finalization fetches confirming redeems per BTC network, verifies blocks and Merkle proofs, and batches finalize requests to the Sui indexer.
  • Extract and simplify splitActiveInactiveTxs to only handle active vs inactive package/deposit-address logic without confirmation calculation and reuse it for finalized mint transactions.
  • Update cron entrypoint to call updateConfirmationsAndFinalize without passing an explicit latest height, since heights are now derived from per-network chain tips in storage.
packages/btcindexer/src/btcindexer.ts
packages/btcindexer/src/index.ts
Extend Sui indexer RPC and storage to support redeem confirmation tracking, status updates, and finalization on Sui with Merkle proofs.
  • Replace empty finalizeRedeem RPC with finalizeRedeems that loads redeem details and Sui clients per network, calls Sui finalizeRedeem transactions, logs results, and marks redeems finalized in storage.
  • Add getConfirmingRedeems, updateRedeemStatus, setRedeemFinalized, and getRedeemWithSetup methods in D1Storage, and extend redeem-related queries and models with light client package/contract fields and confirming-redeem view.
  • Extend Sui client abstraction with finalizeRedeem call that builds and submits the nBTC finalizeRedeem Move transaction including light client arguments and Merkle proof, and validate success via transaction effects.
  • Update Sui indexer RPC interface and mocks to expose the new finalizeRedeems, getConfirmingRedeems, and updateRedeemStatus methods and associated types.
packages/sui-indexer/src/rpc.ts
packages/sui-indexer/src/storage.ts
packages/sui-indexer/src/redeem-sui-client.ts
packages/sui-indexer/src/models.ts
packages/sui-indexer/src/rpc-interface.ts
packages/sui-indexer/src/rpc-mocks.ts
Update and add tests to cover the new splitting logic and revised confirmation/finalization APIs in the BTC indexer test harness.
  • Replace tests for handleReorgs and selectFinalizedNbtcTxs with tests for splitActiveInactiveTxs covering active and inactive package/address combinations.
  • Adjust the skipped updateConfirmationsAndFinalize test to the new no-arg signature and add a TODO-style skeleton for end‑to‑end confirmation/finalization behavior.
  • Extend btcindexer helper test harness to implement the new SuiIndexerRpc methods finalizeRedeems, getConfirmingRedeems, and updateRedeemStatus against the in-memory indexer storage.
packages/btcindexer/src/btcindexer.test.ts
packages/btcindexer/src/btcindexer.helpers.test.ts

Possibly linked issues

  • #UNKNOWN: PR implements redeem confirmation tracking, Sui finalizeRedeem calls, and UTXO marking/removal exactly as described in issue.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sczembor
Copy link
Contributor Author

sczembor commented Feb 3, 2026

@sourcery-ai title

@sourcery-ai sourcery-ai bot changed the title Stan/confirmations feat(btcindexer): finalize redeems on Sui using BTC confirmations Feb 3, 2026
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In categorizeConfirmingTxs each transaction calls isReorged, which in turn hits getBlockHash one-by-one; consider grouping by (blockHeight, network) and fetching hashes once per group to avoid an N-per-block D1 query pattern as the pending set grows.
  • In RPC.finalizeRedeems you fetch redeem details and hit storage per redeemId, then create Sui clients after collecting networks; you could reduce DB and client setup overhead by fetching all redeemIds in a single query (e.g. WHERE redeem_id IN (...)) and constructing the client map once up front.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `categorizeConfirmingTxs` each transaction calls `isReorged`, which in turn hits `getBlockHash` one-by-one; consider grouping by `(blockHeight, network)` and fetching hashes once per group to avoid an N-per-block D1 query pattern as the pending set grows.
- In `RPC.finalizeRedeems` you fetch redeem details and hit storage per `redeemId`, then create Sui clients after collecting networks; you could reduce DB and client setup overhead by fetching all `redeemId`s in a single query (e.g. `WHERE redeem_id IN (...)`) and constructing the client map once up front.

## Individual Comments

### Comment 1
<location> `packages/sui-indexer/src/rpc.ts:39-41` </location>
<code_context>
+		const detailsMap = new Map<number, RedeemRequest>();
+		const networks = new Set<SuiNet>();
+
+		for (const req of requests) {
+			try {
+				const details = await storage.getRedeemWithSetup(req.redeemId);
+				if (details) {
+					detailsMap.set(req.redeemId, details);
</code_context>

<issue_to_address>
**suggestion (performance):** Per-request `getRedeemWithSetup` in a loop will not scale well for large batches.

`finalizeRedeems` issues one `storage.getRedeemWithSetup` call per request inside the loop, causing a linear number of D1 round-trips for large batches. Since all `redeemId`s are known upfront, consider adding a batched query (e.g. `WHERE redeem_id IN (...)`) or at least fetching in parallel (e.g. `Promise.all` with bounded concurrency) to reduce latency and load for large batches.
</issue_to_address>

### Comment 2
<location> `packages/sui-indexer/src/models.ts:167-175` </location>
<code_context>
 	nbtcContract: string;
 }
+
+export interface FinalizeRedeemCall {
+	redeemId: number;
+	proof: string[]; // hex encoded
+	height: number;
+	txIndex: number;
+	nbtcPkg: string;
+	nbtcContract: string;
+	lcContract: string;
+	lcPkg: string;
+}
</code_context>

<issue_to_address>
**suggestion:** `FinalizeRedeemCall.lcPkg` is unused in the Sui client implementation.

`lcPkg` is defined on `FinalizeRedeemCall` but never used in `SuiClientImp.finalizeRedeem`, which only references `nbtcPkg` and `lcContract`. Since `rpc.finalizeRedeems` still populates `lcPkg`, this adds unused data to the call chain. Either remove `lcPkg` from the interface if it’s not needed, or wire it into the transaction builder if it’s intended to be used (e.g., for a separate light client package).

Suggested implementation:

```typescript
export interface FinalizeRedeemCall {
	redeemId: number;
	proof: string[]; // hex encoded
	height: number;
	txIndex: number;
	nbtcPkg: string;
	nbtcContract: string;
	lcContract: string;
}

```

1. Update any code that constructs `FinalizeRedeemCall` (e.g., in `rpc.finalizeRedeems` and any other callers) to stop providing `lcPkg`.
2. If there are types or schemas elsewhere (e.g., API DTOs, JSON schemas) mirroring `FinalizeRedeemCall`, remove `lcPkg` from those as well to keep everything consistent.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Signed-off-by: sczembor <43810037+sczembor@users.noreply.github.com>
Signed-off-by: sczembor <43810037+sczembor@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements BTC confirmation-based finalization for redeem requests on Sui, extending the existing mint finalization flow with unified reorg detection and confirmation handling logic.

Changes:

  • Implements redeem finalization flow that uses BTC Merkle proofs to finalize redeems on Sui once BTC transactions achieve sufficient confirmations
  • Refactors confirmation and reorg detection logic into reusable generic helpers that work for both mints and redeems
  • Adds per-network chain tip tracking to replace global latest height parameter in the BTC indexer cron flow

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/sui-indexer/src/models.ts Added new types for confirming redeems, finalize redeem items, and light client fields; removed dwalletIds from ProposeRedeemCall
packages/sui-indexer/src/storage.ts Added methods to retrieve confirming redeems, update redeem status, finalize redeems, and fetch redeem details with setup info; updated queries to include lc_pkg and lc_contract fields
packages/sui-indexer/src/rpc.ts Implemented finalizeRedeems method that creates Sui clients and calls on-chain finalization for each redeem with proof
packages/sui-indexer/src/rpc-mocks.ts Updated mock implementations to match new RPC interface
packages/sui-indexer/src/rpc-interface.ts Changed finalizeRedeem to finalizeRedeems (array-based); added getConfirmingRedeems and updateRedeemStatus methods
packages/sui-indexer/src/redeem-sui-client.ts Implemented finalizeRedeem method that submits Merkle proof to Sui light client contract
packages/btcindexer/src/index.ts Simplified cron job by removing latestHeight parameter from updateConfirmationsAndFinalize call
packages/btcindexer/src/btcindexer.ts Split updateConfirmationsAndFinalize into processMintingFinalization and processRedeemFinalization; added generic categorizeConfirmingTxs helper; renamed selectFinalizedNbtcTxs to splitActiveInactiveTxs
packages/btcindexer/src/btcindexer.test.ts Updated tests for renamed methods and removed obsolete test cases
packages/btcindexer/src/btcindexer.helpers.test.ts Updated test helpers to support new finalization flow

Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Copy link
Contributor

@robert-zaremba robert-zaremba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pre-approve, but please do self review of this PR and check carefully your changes here.

sczembor and others added 7 commits February 4, 2026 19:52
Co-authored-by: Robert Zaremba <robert@zaremba.ch>
Signed-off-by: sczembor <43810037+sczembor@users.noreply.github.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
@sczembor sczembor merged commit e2e5203 into master Feb 5, 2026
13 checks passed
@sczembor sczembor deleted the stan/confirmations branch February 5, 2026 15:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

nBTC Redeem: BTC transaction confirmations

3 participants